/* Arduino Uno SSTV QR encoder using a VS1003-based audio module
 * --
 * Chris Baird, August 2022 <vk2cjb@gmail.com>
 * Usage as per the GPLv3.
 */
/* code ripped from vs1003_hello by Andrey Karpov
 *
 * VS1003 development board connected thusly to the Arduino Uno:
 * [GND, 5V]
 * XDCS - D6
 * DREQ - D7
 * XRES - D8
 * XCS  - D9
 * SI   - D11
 * SO   - D12
 * SCLK - D13
 */

#include <math.h>		/* for fmodf() */
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <EEPROM.h>
#include <SPI.h>
#include "VS1003.h"
#include "sine.h"
#include "qrcode.h"

char callsign[12] = "N0CALL";
uint8_t sstvmode = 1;
uint16_t lum_on, lum_off, cr_on, cr_off, cb_on, cb_off;
int callsignlen = 0, cursor = 0, qrversion = 0, qrecc = 0;
char linput[55];

const char* qrsizenames[] =
  { "", "21x21", "25x25", "29x29" };
const int screensizes[] =
  { 0, 25, 29, 33 };

QRCode qrcode;
uint8_t qrcodeData[106];  // limited to a max of 29x29 codes for now..

// The B&W Robot modes need doing...
const char* sstvmodenames[] =
  { "PD50","PD90", "PD160", "PD120", "PD180", "PD240", "PD290" };
const int viscodes[] =
  {     93,    99,      98,      95,      96,      97,      94 };
const int sstvmodeheights[] =
  {    256,   256,     400,     496,     496,     496,     616 };
const int scanlens[] =
  {    732,  1362,    1565,     973,    1464,    1956,    1831 };
const float fudges[] =
  {1552.83,2812.5, 3218.08, 2034.18, 3017.34, 4000.50, 3749.60 };

int nframes = 0;


VS1003 player(9, 6, 7, 8); // cs_pin, dcs_pin, dreq_pin, reset_pin

#define samplebytes (2)
#define channels (1)
#define samplerate (8000)
#define datarate (samplerate*channels*samplebytes)
#define TONESIZE (16)	      // using anything else causes distortion
uint16_t tonebuf[TONESIZE];

const uint8_t wavheader[] =
  {
    'R','I','F','F',
    0,0,0,0,                     // wav size (0=play indefinitely)
    'W','A','V','E', 'f','m','t',' ',
    16, 0, 0, 0,                 // fmt_chunk_size
    1, 0,                        // audio format = PCM
    1, 0,                        // num_channels
    samplerate&255, (samplerate>>8)&255, samplerate>>16,0,
    datarate&255, (datarate>>8)&255, datarate>>16,0,
    samplebytes, 0,              // sample_alignment
    samplebytes<<3, 0,
    'd','a','t','a',
    0,0,0,0                      // data size (0=infinite)
  };

// ----------------------------------------------------------------------

void setup (void)
{
  Serial.begin (9600);

  while (!Serial)
    continue;

  if (EEPROM.read (0) != 255)
    for (int i = 0; i < 12; i++)
      callsign[i] = EEPROM.read (i);
  callsignlen = strlen (callsign);

  sstvmode = EEPROM.read (12);
  EEPROM.get (13, lum_on); EEPROM.get (15, lum_off);
  EEPROM.get (17, cr_on); EEPROM.get (19, cr_off);
  EEPROM.get (21, cb_on); EEPROM.get (23, cb_off);

  if ((sstvmode == 255) || (lum_on == 65535) || (lum_off == 65535) ||
      (cr_on == 65535) || (cr_off == 65535) || (cb_on == 65535) ||
      (cb_off == 65535))
    {
      sstvmode = 1;
      lum_on = 2300; lum_off = 1500;
      cr_on = 2300; cr_off = 1900;
      cb_on = 2300; cb_off = 1900;
    }

  //

  SPI.begin();
  player.begin();
  player.setVolume (0);
  player.playChunk (wavheader,sizeof(wavheader));
}

// ----------------------------------------------------------------------

void sampleout (uint16_t freq)
{
  static float iphase = 0;
  static int bp = 0;

  iphase += ((float)SINESIZE*(float)freq) / (float)samplerate;
  iphase = fmodf (iphase,SINESIZE);
  tonebuf[bp] = pgm_read_word (&sine[(int)iphase]);
  nframes++;

  if (++bp == TONESIZE)
    {
      player.playChunk ((const uint8_t*)tonebuf, sizeof (tonebuf));
      bp = 0;
    }
}


void tonegen (float freq, uint16_t samples)
{
  while (samples--)
    sampleout (freq);
}


void vis (uint8_t c)
{
  uint16_t p = 0;

  tonegen (1900, 2400);		// 300ms
  tonegen (1200, 80);		// 10ms
  tonegen (1900, 2400);		// 300ms
  tonegen (1200, 240);		// 30ms

  for (uint16_t i = 1; i != 256; i <<= 1)
    {
      if ((i == 128) && (p & 1))
	c |= 128;

      if (c & i)
	{
	  p++;
	  tonegen (1100, 240);
	}
      else
	tonegen (1300, 240);
    }

  tonegen (1200, 240);
}

// ----------------------------------------------------------------------

void doline (int y, int text_on, int text_off)
{
  static int lasty1 = -1;
  int screensize = screensizes[qrversion], i;
  int y1 = ((y * screensize) / sstvmodeheights[sstvmode]) - 2, xt = -2;
  uint8_t c = 0, bm = 0, ch;
  uint16_t tf = 0, xx = 0, t;
  int dodisp = (y1 == lasty1) ? 0 : 1;
  int scanlen = scanlens[sstvmode];

  for (uint16_t x = 0; x < scanlen; x++)
    {
      if (tf >= 256)
	{
	  bm >>= 1;
	  tf -= 256;
	  xx++;
	}
      tf += ((uint32_t)screensize << 11) / scanlen;

      if (bm == 0)
	{
	  if ((xt < 0) || (xt >= qrcode.size) ||
	      (y1 < 0) || (y1 >= qrcode.size))
	    c = 0;
	  else
	    {
	      c = qrcode_getModule (&qrcode, xt, y1) ? 255 : 0;
	      if (dodisp)
		Serial.print (c ? F("\u2588\u2588") : F("  "));
	    }

	  xt++;
	  bm = 128;
	}

      t = (c & bm) ? text_off : text_on;
      sampleout (t);
    }

  if (dodisp)
    Serial.println();

  lasty1 = y1;
}

// ----------------------------------------------------------------------

void txscreen_pd (void)
{
  int i, t = 0;

  for (i = 300; i < 2300; i += 2) // VOX kick
    tonegen (i, 1);
  for (; i > 300; i -= 2)
    tonegen (i, 1);
  tonegen (0, samplerate >> 4);
  vis (viscodes[sstvmode]);

  for (int y = 0; y < sstvmodeheights[sstvmode]; y += 2)
    {
      int onf = nframes;
      int f = (y*fudges[sstvmode]) - t;

      tonegen (1200, 160+f);
      tonegen (1500, 17);
      doline (y, lum_on, lum_off);
      doline (y, cr_on, cr_off);
      doline (y, cb_on, cb_off);
      doline (y+1, lum_on, lum_off);

      t += nframes - onf;
    }

  tonegen (0,samplerate);
}


void txscreen_ascii (void)
{
  int x, y;

  for (y = 0; y < qrcode.size; y++)
    {
      for (x = 0; x < qrcode.size; x++)
	{
	  int c = qrcode_getModule (&qrcode, x, y);
	  Serial.print (c ? F("\u2588\u2588") : F("  "));
	}
      Serial.println();
    }
}

// ----------------------------------------------------------------------
// Assumes 'byte' content-- QR code does check for numeric/alphanumeric
// See https://github.com/ricmoo/qrcode/ (and likely elsewhere)

const uint8_t qrsizebytes[9] = { 7, 11, 14, 20, 24, 26, 32, 42, 53 };
const uint8_t qrsizemode[9]  = { 1,  1,  2,  2,  3,  2,  3,  3,  3 };
const uint8_t qrsizeecc[9]   = { 3,  2,  3,  2,  3,  1,  2,  1,  0 };

int whatqrtouse (int l)
{
  int b;

  if (l > 53)
    return 0;

  for (b = 0; qrsizebytes[b] < l; b++)
    continue;

  return (qrsizemode[b] << 2) + qrsizeecc[b];
}

// ----------------------------------------------------------------------

void showcolval (int f)
{
  Serial.print ((char)(48 + ((f - 1500) / 100)));
}


int getcolval (char c)
{
  int r = 1500 + 100 * (c - 48);

  if ((r < 1500) || (r > 2300))
    r = -1;
  return r;
}

// ----------------------------------------------------------------------

void loop (void)
{
  // ugly, but uses less ROM+RAM than sprintf'ing
  Serial.print (F("\r\n\r\n// AVEQ - Arduino + VS1003 Exchange QR\r\n// Callsign: "));
  Serial.println (callsign);
  Serial.print (F("// Mode: ")); Serial.println (sstvmodenames[sstvmode]);
  Serial.print (F("// Colours: "));
  showcolval (lum_on); showcolval (cr_on); showcolval (cb_on);
  Serial.print (',');
  showcolval (lum_off); showcolval (cr_off); showcolval (cb_off);
  Serial.println();

  cursor = 0;
  linput[cursor] = 0;

  while (1)
    {
      int i, l, ll;

      l = callsignlen + cursor + 1;
      ll = whatqrtouse(l);

      Serial.print (F("\r(")); Serial.print (l); Serial.print (':');
      Serial.print (ll >> 2); Serial.print (','); Serial.print (ll & 3);
      Serial.print ("): "); linput[cursor] = 0; Serial.print (linput);
      Serial.print (' '); Serial.print (callsign); Serial.print (' ');

      i = strlen (callsign) + 2;
      while (i--)
	Serial.print ('\b');

      while (Serial.available() < 1)
	continue;
      int c = Serial.read();

      //if ((c >= 'a') && (c <= 'z'))
      //  c -= 32;

      if ((c == 8) || (c == 127))
	if (cursor)
	  {
	    cursor--;
	    continue;
	  }

      if ((c > 31) && (c < 127))
	if (cursor < 46)
	  {
	    linput[cursor++] = c;
	    continue;
	  }

      if ((c == '\r') || (c == '\n'))
	break;
    }

  if (strncmp (linput, "SET", 3) == 0)
    {
      if (strncmp (linput+3, "CALL", 4) == 0)
	{
	  int j = 0;

	  for (int i = 8; linput[i] > 32; i++)
	    EEPROM.write (j++, linput[i]);

	  EEPROM.write (j, 0);
	}

      if (strncmp(linput+3, "MODE", 4) == 0)
	{
	  int m = sstvmode;

	  for (int i = 0; i < sizeof(sstvmodenames); i++)
	    if (strcmp(linput+8, sstvmodenames[i]) == 0)
	      m = i;

	  EEPROM.write(12, m);
	}

      if (strncmp(linput+3, "COLS", 4) == 0)
	{
	  lum_on = getcolval (linput[8]); lum_off = getcolval (linput[11]);
	  cr_on = getcolval (linput[9]); cr_off = getcolval (linput[12]);
	  cb_on = getcolval (linput[10]); cb_off = getcolval (linput[13]);

	  if ((lum_on != -1) && (lum_off != -1) && (cr_on != -1) &&
	      (cr_off != -1) && (cb_on != -1) && (cb_off != -1))
	    {
	      if (lum_on <= lum_off)
		Serial.println (F("\r\nYou're being weird, Human."));

	      EEPROM.put (13, lum_on); EEPROM.put (15, lum_off);
	      EEPROM.put (17, cr_on); EEPROM.put (19, cr_off);
	      EEPROM.put (21, cb_on); EEPROM.put (23, cb_off);
	    }
	}

      asm ("jmp 0");
    }

  linput[cursor++] = 32;
  for (int i = 0; i < callsignlen; i++)
    linput[cursor++] = callsign[i];
  linput[cursor] = 0;

  int ll = whatqrtouse(cursor);
  qrversion = ll >> 2;
  qrecc = ll & 3;

  Serial.println();
  Serial.print (F("\r\n> Message: \"")); Serial.print (linput);
  Serial.print (F("\"\r\n> ")); Serial.print (cursor);
  Serial.print (F(" letters -- selecting size "));
  Serial.print (qrsizenames[qrversion]);
  Serial.print (F(", ECC level ")); Serial.println (qrecc);
  Serial.print (F("> Continue (y/N)?"));

  int doit = 0;
  if (ll)
    {
      while (Serial.available() < 1)
	continue;
      int c = Serial.read();
      if ((c == 'y') || (c == 'Y'))
	doit = 1;
    }

  if (!doit)
     Serial.println (F("\r\nNo transmission."));
  else
    {
      Serial.println (F("\r\n> Generating QR"));
      qrcode_initText (&qrcode, qrcodeData, qrversion, qrecc, linput);

      Serial.println (F("> Transmitting"));
      txscreen_pd();
    }
}

// ----------------------------------------------------------------------
